// ==UserScript==
// @name         Clien Liker Chain Block
// @version      0.7
// @match        *.clien.net/service/popup/like/*
// @match        https://www.clien.net/service/board/*
// @connect      clien.net
// ==/UserScript==

(function() {
    'use strict';

    if (window.location.pathname.indexOf('/service/popup/like/') !== 0) return;

    // Nickname shown for one liker. The <a class="nickname"> holds the name in its first plain <span>
    // and the existing memo in span.content_memo, so read only the name span (never .text() of the
    // whole anchor, or an old memo would leak into the new one). Image nicknames keep the name in img alt/title.
    function nicknameOf($el) {
        var name = $el.children('span').not('.content_memo').first().text().replace(/\s+/g, ' ').trim();
        if (!name) {
            var $img = $el.find('img').first();
            name = ($img.attr('alt') || $img.attr('title') || '').trim();
        }
        return name;
    }

    var header = $('h3').text();

    $('h3').html(header+' 차단 <- 클릭');

    $('h3').click(function() {
        var $this = $(this);
        $this.html(header+' 차단 중...');

        var pathname = window.location.pathname;
        var article_num = pathname.split('popup/like/')[1];

        $('.nickname').each(function(index) {
            var onclick = $(this).attr('onclick');
            var id = onclick.split('userInfoPopup(')[1].split(',')[1].split("'")[1];
            var nickname = nicknameOf($(this));
            console.log(id, nickname);

            var params = {
                note: article_num + ' // ' + id + (nickname ? ' ' + nickname : ''),   // e.g. 'park/19256066 // rbgus2733 에이엠디'
                blockMessageYn: true,
                blockArticleYn: true
            };

            var url = API_HOST + '/mypage/memo/block/' + id;

            $.ajax({
                url: url,
                type: 'POST',
                async: false,
                data: {
                    myUserMemo: JSON.stringify(params)
                },
                success: function(result) {
                    if(result){
                        var userId = $('#userId').val();
                        storage.storageUpdatePopup(userId);
                        console.log(userId + ' blocked');
                    }
                },
                error: function(result) {
                    util.log(result);
                    var res = JSON.parse(result.responseText);
                    alert(res.message);
                }
            });
        });

        $this.html(header+' 차단 완료!');

    });

})();

// ---- Feature: join-year badge on /service/board/* list and article pages ----
// Shows each author's join year (two digits, e.g. 2018 -> '18') right after the
// nickname: list rows, the post header and every comment row (nested replies
// included). Login id comes from data-author-id on list/comment rows and from the
// hidden <input id="writer"> for the post header; the join date is read from
// /service/popup/userInfo/basic/<loginId> and cached in localStorage.

(function() {
    'use strict';

    if (window.location.pathname.indexOf('/service/board/') !== 0) return;

    // === pure helpers (no DOM access; keep them Node-testable) ===
    // Badge look & text — the ONLY place to edit how the badge appears.
    var BADGE_CLASS = 'tm-join-year';
    // The second selector outranks the site's '.list_item .list_author .nickname span' rule (0,3,1) so the
    // badge, not the site's nickname image, is the flex item that shrinks when the 7.917em box is full.
    var BADGE_CSS = '.' + BADGE_CLASS + ',.list_item .list_author .nickname .' + BADGE_CLASS +
        '{font-size:0.75em;font-weight:400;color:#999;margin-left:0.25em;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:clip;pointer-events:none}' +
        // Post header / comment rows: the site renders a "님" suffix as span.nickname::after, which is a flex item with
        // order 0. order:1 puts the badge after it ("닉네임님 18"); flex-shrink:0 lets the (ellipsis) name shrink instead of the badge.
        '.post_contact .contact_name .nickname .' + BADGE_CLASS + '{order:1;flex-shrink:0}';
    function badgeText(y, m, d) { return y.slice(-2); }                     // '2018' -> '18'
    function badgeTitle(y, m, d) { return '가입일 ' + y + '-' + m + '-' + d; }
    // Tooltip for span.nickname: set it, or append to a title the site already put there (never duplicate).
    function mergedTitle(existing, t) {
        if (!existing) return t;
        if (existing.indexOf(t) !== -1) return existing;
        return existing + ' / ' + t;
    }

    var JOIN_DATE_RE = /가입일\s*:\s*(\d{4})-(\d{2})-(\d{2})/;
    var NOT_LOGIN_RE = /class="[^"]*\bnot_?login\b/;   // logged-out popup: class="popup notlogin" / class="not_login"
    var YMD_RE = /^\d{4}-\d{2}-\d{2}$/;                 // shape of a cached value

    // Returns 'YYYY-MM-DD' or null. Tags/&nbsp; stripped first so label and value may live in separate tags.
    function parseJoinDate(html) {
        var text = String(html).replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ');
        var m = JOIN_DATE_RE.exec(text);
        if (!m) return null;
        var year = Number(m[1]);
        if (year < 2000 || year > 2099) return null;
        return m[1] + '-' + m[2] + '-' + m[3];
    }
    // === end pure helpers ===

    var PROFILE_PATH = '/service/popup/userInfo/basic/';
    // Where badges go. row = element that identifies the author (gets the DONE_ATTR marker), id(row) = login id or
    // null (-> 'skip'), nick = nickname box inside row that receives the badge. Comment rows include nested replies
    // (.comment_row.re) and rows the site re-renders via getCommentList(); the post header has no data-author-id, so
    // its id is read from the hidden <input id="writer"> that the site's own view.js uses as env.writer.
    function attrAuthorId(el) { return el.getAttribute('data-author-id'); }
    function articleWriterId() {
        var w = document.getElementById('writer');
        var v = w && w.value ? String(w.value).trim() : '';
        return v || null;
    }
    var TARGETS = [
        { row: '[data-role="list-row"][data-author-id]',    id: attrAuthorId,    nick: '.list_author .nickname' },
        { row: '[data-role="comment-row"][data-author-id]', id: attrAuthorId,    nick: '.contact_name .nickname' },
        { row: '[data-role="post-info"]',                   id: articleWriterId, nick: '.contact_name .nickname' }
    ];
    var DONE_ATTR = 'data-tm-join-year';            // pending | done | fail | skip
    var CACHE_KEY = 'tm_clien_join_year_v1';
    var NEG_TTL_MS = 60 * 60 * 1000;                // failures are retried after 1h; successes are kept forever
    var MAX_CONCURRENT = 3;

    function own(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); }

    var cache = loadCache();   // { v: 1, ok: { id: 'YYYY-MM-DD' }, fail: { id: epochMs } }
    var saveTimer = null;

    function loadCache() {
        var data = null;
        try { data = JSON.parse(localStorage.getItem(CACHE_KEY)); } catch (e) { data = null; }
        if (!data || data.v !== 1 || !data.ok || typeof data.ok !== 'object' || !data.fail || typeof data.fail !== 'object') {
            return { v: 1, ok: {}, fail: {} };
        }
        var now = Date.now();
        Object.keys(data.ok).forEach(function(id) {
            if (typeof data.ok[id] !== 'string' || !YMD_RE.test(data.ok[id])) delete data.ok[id];
        });
        Object.keys(data.fail).forEach(function(id) {
            if (!(now - data.fail[id] < NEG_TTL_MS)) delete data.fail[id];
        });
        return data;
    }

    function saveCache() {
        try { localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); } catch (e) { /* quota / privacy mode: cache is best-effort */ }
    }

    function scheduleSave() {
        if (saveTimer) return;
        saveTimer = setTimeout(function() { saveTimer = null; saveCache(); }, 250);
    }

    window.addEventListener('pagehide', function() {
        if (!saveTimer) return;
        clearTimeout(saveTimer);
        saveTimer = null;
        saveCache();
    });

    var debugged = false;
    function debugOnce(msg, err) {
        if (debugged) return;
        debugged = true;
        console.debug('[tm-join-year] ' + msg, err || '');
    }

    function injectCss() {
        if (document.getElementById(BADGE_CLASS + '-style')) return;
        var style = document.createElement('style');
        style.id = BADGE_CLASS + '-style';
        style.textContent = BADGE_CSS;
        (document.head || document.documentElement).appendChild(style);
    }

    var waiting = {};     // loginId -> {el, nick} items waiting for a queued/in-flight fetch
    var queue = [];       // loginIds in DOM order
    var active = 0;
    var loggedOut = false;

    function badge(nickEl, ymd) {
        if (nickEl.querySelector('.' + BADGE_CLASS)) return;
        var p = ymd.split('-');
        var span = document.createElement('span');
        span.className = BADGE_CLASS;
        span.textContent = badgeText(p[0], p[1], p[2]);
        span.setAttribute('title', badgeTitle(p[0], p[1], p[2]));
        // pointer-events:none means hover lands on the parent, so mirror the tooltip there
        // (the inner name span / img keeps its own title, so hovering the name still shows the nickname).
        nickEl.setAttribute('title', mergedTitle(nickEl.getAttribute('title'), badgeTitle(p[0], p[1], p[2])));
        nickEl.appendChild(span);
    }

    // item = { el, nick }: the nickname is looked up when the result lands, so a row the site emptied in the
    // meantime (blocked author -> row.html("")) is simply left without a badge.
    function finishRow(item, ymd) {
        item.el.setAttribute(DONE_ATTR, ymd ? 'done' : 'fail');
        if (!ymd) return;
        var nick = item.el.querySelector(item.nick);
        if (nick) badge(nick, ymd);
    }

    function settle(id, ymd) {
        var items = own(waiting, id) ? waiting[id] : [];
        delete waiting[id];
        items.forEach(function(item) {
            try { finishRow(item, ymd); } catch (e) { debugOnce('badge failed', e); }
        });
    }

    function processRow(row, target) {
        var id = target.id(row);
        var item = { el: row, nick: target.nick };
        row.setAttribute(DONE_ATTR, 'pending');
        if (!id) { row.setAttribute(DONE_ATTR, 'skip'); return; }
        if (own(cache.ok, id)) { finishRow(item, cache.ok[id]); return; }
        var failedAt = own(cache.fail, id) ? cache.fail[id] : 0;
        if (loggedOut || (failedAt && Date.now() - failedAt < NEG_TTL_MS)) { row.setAttribute(DONE_ATTR, 'fail'); return; }
        if (own(waiting, id)) { waiting[id].push(item); return; }          // same author already queued / in flight
        waiting[id] = [item];
        queue.push(id);
        pump();
    }

    function pump() {
        if (loggedOut) { queue.splice(0).forEach(function(id) { settle(id, null); }); return; }
        while (active < MAX_CONCURRENT && queue.length) { active++; fetchJoinDate(queue.shift()); }
    }

    function fetchJoinDate(id) {
        // Chain shape matters: every path (HTTP error, network error, parse miss, not-logged-in)
        // reaches the final .then exactly once, so settle()/active--/pump() run exactly once per id.
        fetch(PROFILE_PATH + encodeURIComponent(id), { credentials: 'include' })
            .then(function(res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.text(); })
            .then(function(html) {
                var ymd = parseJoinDate(html);
                if (ymd) { cache.ok[id] = ymd; delete cache.fail[id]; scheduleSave(); }
                else if (NOT_LOGIN_RE.test(html)) { loggedOut = true; debugOnce('not logged in; join-year badges disabled for this page'); }   // no negative cache
                else { cache.fail[id] = Date.now(); scheduleSave(); debugOnce('profile fetched but no join date found for ' + id); }   // markup differs from the assumed '가입일 :YYYY-MM-DD'
                return ymd;
            })
            .catch(function(err) {
                cache.fail[id] = Date.now(); scheduleSave();
                debugOnce('profile fetch failed', err);
                return null;
            })
            .then(function(ymd) { settle(id, ymd || null); active--; pump(); });
    }

    function scan() {
        for (var t = 0; t < TARGETS.length; t++) {
            var rows = document.querySelectorAll(TARGETS[t].row + ':not([' + DONE_ATTR + '])');
            for (var i = 0; i < rows.length; i++) {
                try { processRow(rows[i], TARGETS[t]); } catch (e) { debugOnce('row failed', e); }
            }
        }
    }

    injectCss();
    scan();

    // Paging is full-page navigation (the script simply re-runs). The observer covers everything the site injects
    // or re-renders by script: comment reload/append ([data-role=comment]), the board list under an article
    // (#listDiv), writer tabs (#writerList_div). Debounced so our own badge insertions coalesce into one no-op scan
    // (the marker attribute keeps already-processed rows out of it).
    var observeTarget = document.body || document.documentElement;
    var scanTimer = null;
    new MutationObserver(function() {
        try {
            if (scanTimer) return;
            scanTimer = setTimeout(function() {
                scanTimer = null;
                try { scan(); } catch (e) { debugOnce('scan failed', e); }
            }, 100);
        } catch (e) { debugOnce('observer failed', e); }
    }).observe(observeTarget, { childList: true, subtree: true });

})();
